package org.foreningsgatan.modules.glcore.graphics;


import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;

import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;

import org.foreningsgatan.modules.glcore.frames.Frames;
import org.foreningsgatan.modules.glcore.graphics.transform.TransformMatrix;

import com.sun.opengl.util.texture.Texture;

/**
 * Copyright (c) 2008 Joakim Back
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * @author Joakim Back
 * 
 */
public class GLGraphics {
	public static float[] BLACK = { 0.0f, 0.0f, 0.0f, 1.0f };
	
	public static float[] WHITE = { 1.0f, 1.0f, 1.0f, 1.0f };

	public static float[] DEFAULT_TEXTUREOFFSET = { 0.0f, 0.0f, 1.0f, 1.0f };

	private static Map<Alignment, float[]> sHorizontalAlignmentOffset;
	
	private static Map<Alignment, float[]> sVerticalAlignmentOffset;
	
	private GL gl;
	
	private TextureManager textureManager;
	
	private Long mCurrentTextureId = null;
	
	private Long mCurrentMultitexture1 = null;
	
	private Long mCurrentMultitexture2 = null;
	
	private Texture mCurrentTexture = null;

	@SuppressWarnings("unused")
	private GLU glu = new GLU();

	private float width;

	private float height;
	
	static {
		sHorizontalAlignmentOffset = new HashMap<Alignment, float[]>();
		sVerticalAlignmentOffset = new HashMap<Alignment, float[]>();
		
		float[] f;
		
		f = new float[] {-1.0f, 0.0f};
		sHorizontalAlignmentOffset.put(Alignment.UPPER_RIGHT, f);
		sHorizontalAlignmentOffset.put(Alignment.MIDDLE_RIGHT, f);
		sHorizontalAlignmentOffset.put(Alignment.LOWER_RIGHT, f);
		
		f = new float[] {-0.5f, 0.5f};
		sHorizontalAlignmentOffset.put(Alignment.UPPER_MIDDLE, f);
		sHorizontalAlignmentOffset.put(Alignment.CENTER, f);
		sHorizontalAlignmentOffset.put(Alignment.LOWER_MIDDLE, f);
		
		f = new float[] {0.0f, 1.0f};
		sHorizontalAlignmentOffset.put(Alignment.UPPER_LEFT, f);
		sHorizontalAlignmentOffset.put(Alignment.MIDDLE_LEFT, f);
		sHorizontalAlignmentOffset.put(Alignment.LOWER_LEFT, f);
		
		f = new float[] {1.0f, 0.0f};
		sVerticalAlignmentOffset.put(Alignment.LOWER_LEFT, f);
		sVerticalAlignmentOffset.put(Alignment.LOWER_MIDDLE, f);
		sVerticalAlignmentOffset.put(Alignment.LOWER_RIGHT, f);
		
		f = new float[] {0.5f, -0.5f};
		sVerticalAlignmentOffset.put(Alignment.MIDDLE_LEFT, f);
		sVerticalAlignmentOffset.put(Alignment.CENTER, f);
		sVerticalAlignmentOffset.put(Alignment.MIDDLE_RIGHT, f);
		
		f = new float[] {0.0f, -1.0f};
		sVerticalAlignmentOffset.put(Alignment.UPPER_LEFT, f);
		sVerticalAlignmentOffset.put(Alignment.UPPER_MIDDLE, f);
		sVerticalAlignmentOffset.put(Alignment.UPPER_RIGHT, f);
	}
	
	private void setBlendMode(BlendMode mode) {
		switch (mode) {
		case ALPHA:
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
			break;
		case ADDITIVE:
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
			break;
		case SUBTRACTIVE:
			/* TODO: Implement SUBTRACTIVE blend function */
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
			break;
		case OPAQUE:
		default:
			gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ZERO);
		}
	}
	
	private void bindTexture(Long textureId) {
		if (textureId == mCurrentTextureId) {
			return;
		}
		
		if (mCurrentTexture != null) {
			mCurrentTexture.disable();
		}
		
		mCurrentTexture = textureManager.getTexture(textureId);
		mCurrentTexture.enable();
		mCurrentTexture.setTexParameterf(GL.GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f);
		mCurrentTexture.bind();
		mCurrentTextureId = textureId;
	}
	
	private float[][] getAlignmentOffset(Alignment align) {
		return new float[][] { 
				sHorizontalAlignmentOffset.get(align), 
				sVerticalAlignmentOffset.get(align) };
	}

	public GLGraphics(float width, float height) {
		this.width = width;
		this.height = height;
	}

	public void setGL(GL gl) {
		this.gl = gl;
	}

	public void setTextureManager(TextureManager textureManager) {
		this.textureManager = textureManager;
	}

	public boolean inView(float x, float y, float margin) {
		return inView(x, y, 0, 0, margin);
	}

	public boolean inView(float x, float y, float w, float h, float margin) {
		x -= margin;
		y -= margin;
		w += margin * 2;
		h += margin * 2;

		return ((x < width) && (x + w > 0) && (y < height) && (y + h > 0));
	}

	public void fillRect(float x, float y, float w, float h, float[] color) {
		y = height - y;

		gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
		gl.glColor4fv(color, 0);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex2f(x, y);
		gl.glVertex2f(x + w, y);
		gl.glVertex2f(x + w, y - h);
		gl.glVertex2f(x, y - h);
		gl.glEnd();
	}

	public void drawTexture(float x, float y, float w, float h, Long textureId, 
			Alignment align, BlendMode mode, float[] rgb, TransformMatrix pMatrix) {
		y = height - y;
		float[][] c = getAlignmentOffset(align);

		bindTexture(textureId);
		setBlendMode(mode);
		
		gl.glPushMatrix();
		gl.glTranslatef(x, y, 0.0f);
		gl.glScalef(w, h, 1.0f);
		// gl.glRotatef(0, 0.0f, 0.0f, 1.0f);
		
		if (pMatrix != null) {
			gl.glMultMatrixf(pMatrix.getMatrix(), 0);
		}
		
		gl.glBegin(GL.GL_POLYGON);
		gl.glColor4f(rgb[0], rgb[1], rgb[2], rgb[3]);
		gl.glTexCoord2f(0f, 0f);
		gl.glVertex2f(c[0][0], c[1][0]);
		gl.glTexCoord2f(1f, 0f);
		gl.glVertex2f(c[0][1], c[1][0]);
		gl.glTexCoord2f(1f, 1f);
		gl.glVertex2f(c[0][1], c[1][1]);
		gl.glTexCoord2f(0f, 1f);
		gl.glVertex2f(c[0][0], c[1][1]);
		gl.glEnd();
		
		gl.glPopMatrix();
	}
	
	public void drawPackedTexture(float x, float y, float w, float h, PackedTexture texture, 
			Alignment align, BlendMode mode, float[] rgb, TransformMatrix pMatrix) {
		y = height - y;
		float[][] c = getAlignmentOffset(align);
		Rectangle2D.Float bounds = texture.getBounds();

		bindTexture(texture.packedTextureId);
		setBlendMode(mode);
		
		gl.glPushMatrix();
		gl.glTranslatef(x, y, 0.0f);
		gl.glScalef(w, h, 1.0f);
		// gl.glRotatef(0, 0.0f, 0.0f, 1.0f);
		
		if (pMatrix != null) {
			gl.glMultMatrixf(pMatrix.getMatrix(), 0);
		}
		
		gl.glBegin(GL.GL_POLYGON);
		gl.glColor4f(rgb[0], rgb[1], rgb[2], rgb[3]);
		gl.glTexCoord2f(bounds.x, bounds.y);
		gl.glVertex2f(c[0][0], c[1][0]);
		gl.glTexCoord2f(bounds.x + bounds.width, bounds.y);
		gl.glVertex2f(c[0][1], c[1][0]);
		gl.glTexCoord2f(bounds.x + bounds.width, bounds.y + bounds.height);
		gl.glVertex2f(c[0][1], c[1][1]);
		gl.glTexCoord2f(bounds.x, bounds.y + bounds.height);
		gl.glVertex2f(c[0][0], c[1][1]);
		gl.glEnd();
		
		gl.glPopMatrix();
	}
	
	public void drawTexture(float x, float y, float w, float h, PackedTexture texture,
			BlendMode mode, float[] rgb) {
		drawPackedTexture(x, y, w, h, texture, Alignment.UPPER_LEFT, mode, rgb, null);
	}

	public void drawTexture(float x, float y, float w, float h, Long textureId,
			BlendMode mode) {
		drawTexture(x, y, w, h, textureId, Alignment.UPPER_LEFT, mode, WHITE, null);
	}

	public void drawTexture(float x, float y, float w, float h, Long textureId) {
		drawTexture(x, y, w, h, textureId, Alignment.UPPER_LEFT, BlendMode.ALPHA, WHITE, null);
	}

	public void drawFrames(float x, float y, Frames frames, Alignment align,
			float scale, float[] rgb, BlendMode mode, TransformMatrix pMatrix) {
		PackedTexture texture = frames.getNextFrame();

		Texture tex = textureManager.getTexture(texture.packedTextureId);
		
		float w = tex.getImageWidth() * texture.bounds.width * scale;

		float h = tex.getImageHeight() * texture.bounds.height * scale;
		
		drawPackedTexture(x, y, w, h, frames.getNextFrame(), align, mode, rgb, pMatrix);
	}
	
	public void drawFrames(float x, float y, Frames frames, Alignment align,
			float scale, float[] rgb, BlendMode mode) {
		drawFrames(x, y, frames, align, scale, rgb, mode, null);
	}
	
	public void enableMultiTexturing() {
		// Set up the first texture unit to interpolate using crossbar extension

		gl.glActiveTexture(GL.GL_TEXTURE0);
		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_COMBINE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_RGB, GL.GL_INTERPOLATE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE0_RGB, GL.GL_TEXTURE1);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE1_RGB, GL.GL_TEXTURE0);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE2_RGB, GL.GL_PRIMARY_COLOR);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_OPERAND2_RGB, GL.GL_SRC_ALPHA);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_ALPHA, GL.GL_INTERPOLATE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE0_ALPHA, GL.GL_TEXTURE1);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE1_ALPHA, GL.GL_TEXTURE0);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE2_ALPHA,
				GL.GL_PRIMARY_COLOR);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_OPERAND2_ALPHA, GL.GL_SRC_ALPHA);

		// Set up the second texture unit for primary color tinting
		gl.glActiveTexture(GL.GL_TEXTURE1);
		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_COMBINE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_RGB, GL.GL_MODULATE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE0_RGB, GL.GL_PRIMARY_COLOR);
		float[] constant = { 1, 1, 1, 1 };
		gl.glTexEnvfv(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_COLOR, constant, 0);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_ALPHA, GL.GL_REPLACE);
		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_SOURCE0_ALPHA, GL.GL_CONSTANT);
	}

	public void disableMultiTexture() {
		// Turn the second multitexture pass off
		gl.glActiveTexture(GL.GL_TEXTURE1);
		gl.glDisable(GL.GL_TEXTURE_2D);

		// Turn the first multitexture pass off
		gl.glActiveTexture(GL.GL_TEXTURE0);
		gl.glDisable(GL.GL_TEXTURE_2D);

		gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
		
		mCurrentMultitexture1 = null;
		mCurrentMultitexture2 = null;
	}
	
	public void drawMultiTexture(float x, float y, float w, float h,
			Long textureId1, Long textureId2, boolean[] isPrimary,
			float textureOffset[], float[][] color) {

		y = height - y;

		/* Enable Multitexturing */

		if (textureId1 != mCurrentMultitexture1) {
			Texture tex1 = textureManager.getTexture(textureId1);
			gl.glActiveTexture(GL.GL_TEXTURE0);
			gl.glBindTexture(GL.GL_TEXTURE_2D, tex1.getTextureObject());
			mCurrentMultitexture1 = textureId1;
		}
		
		if (textureId2 != mCurrentMultitexture2) {
			Texture tex2 = textureManager.getTexture(textureId2);
			gl.glActiveTexture(GL.GL_TEXTURE1);
			gl.glBindTexture(GL.GL_TEXTURE_2D, tex2.getTextureObject());
			mCurrentMultitexture2 = textureId2;
		}

		/*
		 * Triangle Fan Rendering
		 * 
		 * Center, Lower Left, Upper Left, Upper Right, Lower Right
		 */

		float tx1 = textureOffset[0];
		float tx2 = textureOffset[0] + textureOffset[2];
		float ty1 = textureOffset[1];
		float ty2 = textureOffset[1] + textureOffset[3];

		// Calculate blending of the center vertex

		float p = 0.0f;

		for (int i = 0; i < isPrimary.length; i++) {
			if (!isPrimary[i])
				p += 0.25f;
		}
		
		// Calculate color of the center vertex

		float midColor[] = new float[] {
				(color[0][0] + color[1][0] + color[2][0] + color[3][0]) / 4,
				(color[0][1] + color[1][1] + color[2][1] + color[3][1]) / 4,
				(color[0][2] + color[1][2] + color[2][2] + color[3][2]) / 4 };

		gl.glBegin(GL.GL_TRIANGLE_FAN);
		{
			gl.glColor4f(midColor[0], midColor[1], midColor[2], p);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, (tx2 + tx1) / 2,
					(ty2 + ty1) / 2);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, (tx2 + tx1) / 2,
					(ty2 + ty1) / 2);
			gl.glVertex2f(x + w / 2, y - h / 2);

			gl.glColor4f(color[2][0], color[2][1], color[2][2],
					isPrimary[2] ? 0.0f : 1.0f);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, tx1, ty2);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, tx1, ty2);
			gl.glVertex2f(x, y - h);

			gl.glColor4f(color[0][0], color[0][1], color[0][2],
					isPrimary[0] ? 0.0f : 1.0f);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, tx1, ty1);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, tx1, ty1);
			gl.glVertex2f(x, y);

			gl.glColor4f(color[1][0], color[1][1], color[1][2],
					isPrimary[1] ? 0.0f : 1.0f);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, tx2, ty1);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, tx2, ty1);
			gl.glVertex2f(x + w, y);

			gl.glColor4f(color[3][0], color[3][1], color[3][2],
					isPrimary[3] ? 0.0f : 1.0f);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, tx2, ty2);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, tx2, ty2);
			gl.glVertex2f(x + w, y - h);

			gl.glColor4f(color[2][0], color[2][1], color[2][2],
					isPrimary[2] ? 0.0f : 1.0f);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE0, tx1, ty2);
			gl.glMultiTexCoord2f(GL.GL_TEXTURE1, tx1, ty2);
			gl.glVertex2f(x, y - h);
		}
		gl.glEnd();
	}
	
	public void reset() {
		if (mCurrentTextureId != null) {
			mCurrentTexture.disable();
			mCurrentTextureId = null;
		}
	}

	public void enableBlend() {
		gl.glEnable(GL.GL_BLEND);
	}

	public void disableBlend() {
		gl.glDisable(GL.GL_BLEND);
	}

	public float getWidth() {
		return width;
	}

	public float getHeight() {
		return height;
	}
}
